/**********************************************************************\

 Copyright 2012 Diggory Blake

 This file is part of klip-engine

 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.

\**********************************************************************/

#include "ksystemlinux.h"
#include "kencoding.h"
#include <time.h>

kSystemLinux::kSystemLinux() {
	_display = XOpenDisplay(0);
	_context = XUniqueContext();
	_root = DefaultRootWindow(_display);
	_wm_protocols = XInternAtom(_display, "WM_PROTOCOLS", False);
	_wm_delete_window = XInternAtom(_display, "WM_DELETE_WINDOW", False);
	_motif_wm_hints = XInternAtom(_display, "_MOTIF_WM_HINTS", False);
	_net_wm_state = XInternAtom(_display, "_NET_WM_STATE", False);
	_net_wm_state_fullscreen = XInternAtom(_display, "_NET_WM_STATE_FULLSCREEN", False);
	XkbSetDetectableAutoRepeat(_display, True, 0);
}
kSystemLinux::~kSystemLinux() {
	XCloseDisplay(_display);
}
inline keMouseButtons getMouseButtonFromX(int button) {
	switch (button) {
	case Button1:
		return keMouseButton_Left;
	case Button2:
		return keMouseButton_Right;
	case Button3:
		return keMouseButton_Middle;
	}
	return keMouseButton_None;
}
inline keKeyCode getKeyCodeFromX(int keycode) {
	return (keKeyCode)keycode;
}
void kSystemLinux::onUpdate(float delta) {
	for (auto i = _windows.begin(); i != _windows.end(); i++) {
		(*i)->update();
	}

	XEvent e;
	while (XEventsQueued(_display, QueuedAlready)) {
		XNextEvent(_display, &e);

		kWindowLinux* window = 0;
		XFindContext(_display, e.xany.window, _context, (XPointer*)&window);
		
		switch (e.type) {
		case ClientMessage:
			if (e.xclient.message_type == _wm_protocols && e.xclient.data.l[0] == _wm_delete_window)  {
				bool deleteWnd = true;

				CALL_DELEGATE(window->onDestroy, window, &deleteWnd);
				
				if (deleteWnd)
					window->destroy();
			}
			break;
		case ConfigureNotify:
			{
				kVec2i newSize(e.xconfigure.width, e.xconfigure.height);
				if (window->_size != newSize) {
					window->_size = newSize;

					CALL_DELEGATE(window->onResize, window, window->_size);
				}
				kVec2i pos;
				Window temp;
				XTranslateCoordinates(_display, window->_wnd, _root, 0, 0, &pos.x, &pos.y, &temp);
				if (window->_pos != pos) {
					window->_pos = pos;

					CALL_DELEGATE(window->onMove, window, window->_pos);
				}
			}
			break;
		case MotionNotify:
			{
				kVec2i pos(e.xmotion.x, e.xmotion.y);
				window->_mouseInput.updateMousePos(pos);
			}
			break;
		case ButtonPress:
			window->_mouseInput.updateMouseButtons(getMouseButtonFromX(e.xbutton.button), true);
			break;
		case ButtonRelease:
			window->_mouseInput.updateMouseButtons(getMouseButtonFromX(e.xbutton.button), false);
			break;
		case KeyPress:
			{
				keKeyCode keyCode = getKeyCodeFromX(e.xkey.keycode);
				window->_keyboardInput.updateKey(keyCode, true);
				CALL_DELEGATE(window->_keyboardInput.onKeyPress, window, keyCode);

				char str[25];
				KeySym keysym;
				int len = XLookupString(&e.xkey, str, 25, &keysym, NULL);
				if (len > 0) {
					std::string srcStr(str, len);
					std::tstring destStr;
					kEncodingConverter<char, tchar>::convert(srcStr, destStr);
					for (auto i = destStr.begin(); i != destStr.end(); i++) {
						CALL_DELEGATE(window->_keyboardInput.onKeyChar, window, *i);
					}
				}
			}
			break;
		case KeyRelease:
			window->_keyboardInput.updateKey(getKeyCodeFromX(e.xkey.keycode), false);
			break;
		case EnterNotify:
			window->_mouseInput.updateMouseOver(true);
			break;
		case LeaveNotify:
			window->_mouseInput.updateMouseOver(false);
			break;
		case KeymapNotify:
			XRefreshKeyboardMapping(&e.xmapping);
			break;
		}
	}
}
unsigned kSystemLinux::getTime() {
	timespec t;
	clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &t);

	return t.tv_nsec/1000000 + t.tv_sec*1000;
}
kWindow* kSystemLinux::createWindow(const std::tstring& title) {
	int att[] = { GLX_RGBA, GLX_DOUBLEBUFFER, None };
	XSetWindowAttributes swa = {0};
	swa.event_mask = StructureNotifyMask | KeyPressMask | KeyReleaseMask | ButtonPressMask | ButtonReleaseMask | PointerMotionMask | EnterWindowMask | LeaveWindowMask | KeymapStateMask;
	
	kWindowLinux* window = new kWindowLinux(this);

	window->_vi = glXChooseVisual(_display, DefaultScreen(_display), att);
	swa.colormap = XCreateColormap(_display, _root, window->_vi->visual, AllocNone);
	window->_wnd = XCreateWindow(_display, _root, 0, 0, window->_size.x, window->_size.y, 0, window->_vi->depth, InputOutput, window->_vi->visual, CWEventMask|CWColormap, &swa);
	XSaveContext(_display, window->_wnd, _context, (XPointer)window);
	XSetWMProtocols(_display, window->_wnd, &_wm_delete_window, 1);

	std::string titleStr;
	kEncodingConverter<tchar, char>::convert(title, titleStr);
	XStoreName(_display, window->_wnd, titleStr.c_str());

	return window;
}

kWindowLinux::~kWindowLinux() {
	_system->_windows.erase(this);
}
kWindowLinux::kWindowLinux(kSystemLinux* system) {
	_system = system;
	_pos.x = 0;
	_pos.y = 0;
	_size.x = 640;
	_size.y = 480;
	_options = keWindow_None;
	_system->_windows.insert(this);
	_mouseInput.setSender(this);
	_keyboardInput.setSender(this);
}
void kWindowLinux::update() {
	_mouseInput.preUpdate();
	_keyboardInput.preUpdate();
}
kHandle kWindowLinux::getHandle() {
	return (kHandle)_wnd;
}
kMouseInput* kWindowLinux::getMouseInput() {
	return &_mouseInput;
}
kKeyboardInput* kWindowLinux::getKeyboardInput() {
	return &_keyboardInput;
}
void kWindowLinux::close() {
	XEvent e = {0};
	e.type = ClientMessage;
	e.xany.window = _wnd;
	e.xclient.message_type = _system->_wm_protocols;
	e.xclient.data.l[0] = _system->_wm_delete_window;
	XSendEvent(_system->_display, _wnd, False, 0, &e);
}
void kWindowLinux::destroy() {
	XDeleteContext(_system->_display, _wnd, _system->_context);
	XDestroyWindow(_system->_display, _wnd);

	if (_options & keWindow_Fullscreen) {
		XF86VidModeSwitchToMode(_system->_display, DefaultScreen(_system->_display), &_defaultMode);
	}

	delete this;
}
kHandle kWindowLinux::createGLContext() {
	GLXContext context = glXCreateContext(_system->_display, _vi, 0, GL_TRUE);
	glXMakeCurrent(_system->_display, _wnd, context);
	return (kHandle)context;
}
void kWindowLinux::deleteGLContext(kHandle context) {
	glXDestroyContext(_system->_display, (GLXContext)context);
}
void kWindowLinux::activateGLContext(kHandle context) {
	glXMakeCurrent(_system->_display, _wnd, (GLXContext)context);
}
void kWindowLinux::setOptions(keWindowOptions options) {
	if (!(options & keWindow_Visible)) {
		XUnmapWindow(_system->_display, _wnd);
	}

	XSizeHints hints = {0};
	hints.flags = PMinSize | PMaxSize;
	if ((options & keWindow_Resizable) || (options & keWindow_Fullscreen)) {
		hints.max_width = hints.max_height = 0xFFFF;
		hints.min_width = hints.min_height = 0x0000;
	} else {
		hints.max_width = hints.min_width = _size.x;
		hints.max_height = hints.min_height = _size.y;
	}
	XSetWMNormalHints(_system->_display, _wnd, &hints);

	if ((options & keWindow_Borderless) || (options & keWindow_Fullscreen)) {
		kSystemLinux::MotifHints hints = {0};
		hints.flags = 2;
		hints.decorations = 0;
		XChangeProperty(_system->_display, _wnd, _system->_motif_wm_hints, _system->_motif_wm_hints, 32, PropModeReplace, (unsigned char *)&hints, 5);

		if (options & keWindow_Fullscreen) {
			int numModes = 0;
			XF86VidModeModeInfo** modes = 0;
			XF86VidModeGetAllModeLines(_system->_display, DefaultScreen(_system->_display), &numModes, &modes);
			_defaultMode = *modes[0];

			int bestMode = 0;
			int bestDist = 0x7FFFFFFF;
			for (int i = 0; i < numModes; i++) {
				int xdist = modes[i]->hdisplay - _size.x;
				int ydist = modes[i]->vdisplay - _size.y;
				int dist = xdist*xdist + ydist*ydist;

				if (dist < bestDist) {
					bestDist = dist;
					bestMode = i;
				}
			}
			XF86VidModeSwitchToMode(_system->_display, DefaultScreen(_system->_display), modes[bestMode]);
			XF86VidModeSetViewPort(_system->_display, DefaultScreen(_system->_display), 0, 0);
			XMoveResizeWindow(_system->_display, _wnd, 0, 0, _size.x, _size.y);
			XMapRaised(_system->_display, _wnd);
			XGrabPointer(_system->_display, _wnd, True, 0, GrabModeAsync, GrabModeAsync, _wnd, 0L, CurrentTime);
			XGrabKeyboard(_system->_display, _wnd, False, GrabModeAsync, GrabModeAsync, CurrentTime);
			XFree(modes);

			_system->waitForEvent<MapNotify>(this);

			XEvent xev = {0};

			xev.type = ClientMessage;
			xev.xclient.window = _wnd;
			xev.xclient.message_type = _system->_net_wm_state;
			xev.xclient.format = 32;
			xev.xclient.data.l[0] = 1;
			xev.xclient.data.l[1] = _system->_net_wm_state_fullscreen;
			xev.xclient.data.l[2] = 0;	/* no second property to toggle */
			xev.xclient.data.l[3] = 1;	/* source indication: application */
			xev.xclient.data.l[4] = 0;	/* unused */

			XSendEvent(_system->_display, _system->_root, 0, SubstructureRedirectMask | SubstructureNotifyMask, &xev);
		}
	} else if (_options & keWindow_Fullscreen) {
		XEvent xev = {0};

		xev.type = ClientMessage;
		xev.xclient.window = _wnd;
		xev.xclient.message_type = _system->_net_wm_state;
		xev.xclient.format = 32;
		xev.xclient.data.l[0] = 0;
		xev.xclient.data.l[1] = _system->_net_wm_state_fullscreen;
		xev.xclient.data.l[2] = 0;	/* no second property to toggle */
		xev.xclient.data.l[3] = 1;	/* source indication: application */
		xev.xclient.data.l[4] = 0;	/* unused */

		XSendEvent(_system->_display, _system->_root, 0, SubstructureRedirectMask | SubstructureNotifyMask, &xev);

		XF86VidModeSwitchToMode(_system->_display, DefaultScreen(_system->_display), &_defaultMode);
	}

	if ((options & keWindow_Visible) && !(options & keWindow_Fullscreen)) {
		XMapWindow(_system->_display, _wnd);
		_system->waitForEvent<MapNotify>(this);
	} 
	_options = options;

	CALL_DELEGATE(onChange, this, _options);
}
void kWindowLinux::setSize(const kVec2i& size) {
	XResizeWindow(_system->_display, _wnd, size.x, size.y);
	_system->waitForEvent<ConfigureNotify>(this);
}
void kWindowLinux::setPos(const kVec2i& pos) {
	XMoveWindow(_system->_display, _wnd, pos.x, pos.y);
	_system->waitForEvent<ConfigureNotify>(this);
}
void kWindowLinux::setTitle(const std::tstring& title) {
	XStoreName(_system->_display, _wnd, title.c_str());
}
void kWindowLinux::swapGLBuffers() {
	glXSwapBuffers(_system->_display, _wnd);
}
kVec2i kWindowLinux::getPos() {
	return _pos;
}
kVec2i kWindowLinux::getSize() {
	return _size;
}
keWindowOptions kWindowLinux::getOptions() {
	return _options;
}
